In [4]:
import pandas as pd

from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly
import plotly.graph_objs as go
from datetime import date, timedelta
import plotly.express as px
import numpy as np
In [5]:
df = pd.read_csv('../data/report_task_1.csv')
In [6]:
df.head()
Out[6]:
Hour DC Bids Impressions Spent
0 2020-01-01 6:00:00 EU 3757709.0 828710.0 375448.0
1 2020-01-01 6:00:00 US 3232546.0 597172.0 720578.0
2 2020-01-01 7:00:00 EU 4246966.0 957297.0 404218.0
3 2020-01-01 7:00:00 US 3112665.0 506980.0 564765.0
4 2020-01-01 8:00:00 EU 4768835.0 1040946.0 452959.0
In [7]:
df['DateTime'] = df['Hour'].astype('datetime64[ns]')
df['Hour'] = df['DateTime'].dt.hour
df['Date'] = df['DateTime'].dt.date
In [8]:
df.sort_values(['DC', 'Date', 'Hour'], inplace=True)

Исходя из описания тестового задания мы можем сделать следующие выводы:
Инцидент А -- это отсутствие данных в колонках Bids, Impressions и Spent (если мы не отправляем запросы на пратнера, то он на нас не бидит, следовательно у нас нет показов, и заработанных денег)
Инцидент Б -- это отсутствие данных в колонках Impressions и Spent (партнер делает биды, но мы не учитываем показы)

P.S. В случае Инцидент Б необходимо понимать кто является источником данных для колонки Impressions. Если это сервис аналитики, то предположение выше верно. Если клиенты, то у нас будет другая картина (возможно, в этом случае мы не учитываем не только показ, но и ставку, и в таком случае у нас будет низкое количество ставок и большое показов)

Проверим это предположение:

In [9]:
# Инцидент А
df[df['Bids'].isna()].head()
Out[9]:
Hour DC Bids Impressions Spent DateTime Date
1935 16 EU NaN 1061875.0 609318.0 2020-02-10 16:00:00 2020-02-10
4005 19 EU NaN 1000960.0 654004.0 2020-03-24 19:00:00 2020-03-24
4007 20 EU NaN 1057341.0 692368.0 2020-03-24 20:00:00 2020-03-24
4009 21 EU NaN 1027347.0 626513.0 2020-03-24 21:00:00 2020-03-24
4011 22 EU NaN 963456.0 538592.0 2020-03-24 22:00:00 2020-03-24
In [10]:
# Инцидент Б
df[df['Impressions'].isna()].head()
Out[10]:
Hour DC Bids Impressions Spent DateTime Date
4346 12 EU 8408043.0 NaN NaN 2020-01-10 12:00:00 2020-01-10
4347 11 EU 1359608.0 NaN NaN 2020-01-30 11:00:00 2020-01-30
4349 12 EU 3621884.0 NaN NaN 2020-01-30 12:00:00 2020-01-30
4348 11 US 727206.0 NaN NaN 2020-01-30 11:00:00 2020-01-30
4350 12 US 2367522.0 NaN NaN 2020-01-30 12:00:00 2020-01-30

Действительно, у нас полностью подтверждается Инцидент Б, но есть проблема с Инцидентом А:
Мы видим наличие показов и прибыли, хотя не должны.
Для понимания такого поведения, нужна дополнительная консультация с бизнесом/разработчиками. Возможно, у нас есть другая проблема, о которой мы не подозреваем (например, наш сервис анатилики не записывает биды).

В рамках тестового задания, упраздним этот процесс и удалим данные в колонках Impressions и Spent, в тех случаях, когда остутствует значение в колонке Bids:

In [11]:
df.loc[df['Bids'].isna(), ['Impressions', 'Spent']] = np.nan

Посмотрим на то, как ведут себя соотношения показов к бидам, и сколько денег приносит нам 1 бид и 1 показ:

In [12]:
df['Spent_diff_Bids'] = df['Spent'] / df['Bids']
df['Spent_diff_Impressions'] = df['Spent'] / df['Impressions']
df['Impressions_diff_Bids'] = df['Impressions'] / df['Bids']
In [13]:
# графики интерактивные, можно выделять определенные диапазоны и смотреть их детальнее
for metrics in ['Spent_diff_Bids', 'Spent_diff_Impressions','Impressions_diff_Bids']:
    fig = px.line(df, x='DateTime', y=metrics, color='DC', title=f'{metrics}' )
    fig.show()

Как мы видим, у нас действительно присутствуют случаи, когда соотношения показов к ставкам выше 1. Выделим их в Инцидент В
В таком случае, востановим данные не только для пропущенных значений, но и для тех, когда Impresson/Bids > 1 (в дальнейшем сюда можно будет добавить и другие аномальные значения. Например те, которые значительно выше среднего за предыдущие несколько дней)

Для востановленния данных воспользуемся следующим подходом: для каждого дата центра, в рамках кажодого часа просчитаем скользящее оконное среднее значение за последние 5 дней, но не менее 3-ох.

In [14]:
for i in ['A', 'B', 'C']:
    df[f'Incident_{i}'] = False
    
df.loc[df['Bids'].isna(), 'Incident_A'] = True
df.loc[(df['Incident_A']==False) & (df['Impressions'].isna()), 'Incident_B'] = True
df.loc[df['Impressions_diff_Bids']>1, 'Incident_C'] = True
In [15]:
dataframe = list()
metrics = ['Bids',  'Impressions_diff_Bids', 'Spent_diff_Bids', 'Spent_diff_Impressions']
metrics_rename = {i:f'{i}_rolling' for i in metrics}
for dc in ['US',  'EU']:
    for hour in range(0,24):
        temp = df[(df['Hour'] == hour)&(df['DC'] == dc)][metrics + ['Date']].reset_index(drop=True).copy()
        temp[metrics] = temp[metrics].rolling(5, min_periods=3, win_type='gaussian').mean(std=3)
        temp['Hour'] = hour
        temp['DC'] = dc
        temp.rename(columns=metrics_rename, inplace=True)
        dataframe.append(temp.fillna(0))
In [16]:
temp = pd.concat(dataframe)
filled_dataset = df.copy()
filled_dataset = pd.merge(left=filled_dataset, right=temp, how='left', on=['Hour', 'DC', 'Date'])

Востановим данные для прощуенных значений

In [17]:
for m in metrics:
    fd_na = filled_dataset[m].isna()
    filled_dataset.loc[fd_na, m] = filled_dataset.loc[fd_na, f'{m}_rolling']
    filled_dataset.loc[fd_na, 
                       'Impressions'] = filled_dataset.loc[fd_na, 
                                                           'Bids'] * filled_dataset.loc[fd_na, 
                                                                                        'Impressions_diff_Bids']
    filled_dataset.loc[fd_na, 
                       'Spent'] = filled_dataset.loc[fd_na, 
                                                           'Bids'] * filled_dataset.loc[fd_na, 
                                                                                        'Spent_diff_Bids']
for c in ['Spent', 'Impressions', 'Bids']:
    filled_dataset[c] = filled_dataset[c].astype('int32')

Наглядно посмотрим на то, как заполнились данные в один из период, где они отсустуствовали:

In [18]:
describe = filled_dataset[(filled_dataset['Date']>=date(2020, 3, 25)) & (filled_dataset['Date']<=date(2020, 4, 2))
                         &(filled_dataset['DC']=='US')]
for m in ['Spent_diff_Impressions']:
    fig = px.line(describe, x='DateTime', y=m, title=f'{m}' )
    fig.show()
In [19]:
describe = df[(df['Date']>=date(2020, 3, 25)) & (df['Date']<=date(2020, 4, 2))
                         &(df['DC']=='US')]
for m in ['Spent_diff_Impressions']:
    fig = px.line(describe, x='DateTime', y=m, title=f'{m}' )
    fig.show()
In [25]:
def calculate_metrics(df_t, name):
    for dc in ['US', 'EU']:
        temp = df_t[df_t['DC']==dc].copy()
        temp['Delta_between_incident'] = temp['DateTime'] - temp['DateTime'].shift(1)
        meen_days_between = temp[temp['Delta_between_incident']>timedelta(hours=1)]['Delta_between_incident'].mean()
        incident_count = temp[temp['Delta_between_incident']>timedelta(hours=1)]['Delta_between_incident'].count()
        incident_hour = temp[name].sum()
        incident_spent = int(temp['Spent'].sum())
        print(f'''DC: {dc}
            Incident hours: {incident_hour}
            Incident spent: {incident_spent}
            Days incident between: {meen_days_between}
            Incident count: {incident_count + 1}
            Mean hours: {incident_hour/(incident_count + 1)}''')

Подсчитаем количество часов и потерь от Инцидента А

In [21]:
incident_a = filled_dataset[filled_dataset[f'Incident_A']==True]
calculate_metrics(incident_a, 'Incident_A')
DC: US
            Incident hours: 16
            Incident spent: 27790070
            Days incident between: 24 days 09:00:00
            Incident count: 4
            Mean hours: 4.0
DC: EU
            Incident hours: 14
            Incident spent: 7602370
            Days incident between: 43 days 03:00:00
            Incident count: 2
            Mean hours: 7.0

Подсчитаем количество часов и потерь от Инцидента Б

In [22]:
incident_b = filled_dataset[filled_dataset[f'Incident_B']==True].copy()
calculate_metrics(incident_b, 'Incident_B')
DC: US
            Incident hours: 7
            Incident spent: 13192313
            Days incident between: 59 days 13:00:00
            Incident count: 2
            Mean hours: 3.5
DC: EU
            Incident hours: 3
            Incident spent: 1275548
            Days incident between: 19 days 23:00:00
            Incident count: 2
            Mean hours: 1.5

Подсчитаем количество часов и потерь от Инцидента В

Продалжая логику, описанную выше, мы считаем: что не все ставки и доход от них был записан, а прибыль(Spent) и показы -- это то, что мы получили от клиентов.
Следовательно, наши потери:
(прибыль) - (востановленная прибыль от ставки) * (количество бидов которое мы посчитали)

In [26]:
incident_c = filled_dataset[filled_dataset[f'Incident_C']==True].copy()
incident_c['Spent'] = incident_c['Spent'] - incident_c['Bids'] * incident_c['Spent_diff_Bids_rolling']
calculate_metrics(incident_c, 'Incident_C')
DC: US
            Incident hours: 5
            Incident spent: 4957481
            Days incident between: 21 days 09:20:00
            Incident count: 4
            Mean hours: 1.25
DC: EU
            Incident hours: 2
            Incident spent: 940788
            Days incident between: 61 days 01:00:00
            Incident count: 2
            Mean hours: 1.0